// <copyright file="RequestSecurityToken.cs" company="SharpSTS">
// Copyright (c) 2007, 2008 All Right Reserved, http://sharpsts.com/
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Barry Dorrans</author>
// <email>barryd@idunno.org</email>
// <date>2008-06-16</date>
// <summary>Parses the RST message</summary>

namespace SharpSTS
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Globalization;
    using System.IdentityModel.Tokens;
    using System.ServiceModel;
    using System.ServiceModel.Security;
    using System.Xml;

    using Properties;

    /// <summary>
    /// Represents a request for a security token message.
    /// </summary>
    internal sealed class RequestSecurityToken
    {
        #region Constants
        /// <summary>
        /// The default token type to issue.
        /// </summary>
        private const string DefaultTokenType = "urn:oasis:names:tc:SAML:1.0:assertion";
        #endregion

        #region Fields        
        /// <summary>
        /// Holds the delegates for the WSTrust section parsing.
        /// </summary>
        private readonly Dictionary<string, ElementHandler> wsTrustElements = new Dictionary<string, ElementHandler>();

        /// <summary>
        /// Holds the delegates for the WSIdentity section parsing.
        /// </summary>
        private readonly Dictionary<string, ElementHandler> wsIdentityElements = new Dictionary<string, ElementHandler>();
        
        /// <summary>
        /// Holds the expected values for various attributes.
        /// </summary>
        private static readonly Dictionary<string, string> expectedValues = new Dictionary<string, string> 
            {        
                { Constants.WSTrust.Elements.RequestType, Constants.WSTrust.RequestTypes.Issue },
                { Constants.WSTrust.Elements.TokenType, Constants.WSTrust.TokenTypes.Saml10Assertion },
                { Constants.WSTrust.Elements.CanonicalizationAlgorithm, Constants.XmlCanonicalization.Algorithms.ExcC14n },
                { Constants.WSTrust.Elements.KeySize, Constants.Numbers.TwoThousandFortyEight },
                { Constants.WSTrust.Elements.EncryptWith, Constants.XmlEnc.Algorithms.RsaOaepMgf1p },
                { Constants.WSTrust.Elements.SignWith, Constants.XmlDSig.Algorithms.RsaSha1 }
            };
        
        /// <summary>
        /// Holds the delegates for the WSPolicy section parsing.
        /// </summary>
        private readonly Dictionary<string, ElementHandler> wsPolicyElements = new Dictionary<string, ElementHandler>();

        /// <summary>
        /// The token type requested.
        /// </summary>
        private string tokenType;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="RequestSecurityToken"/> class.
        /// </summary>
        /// <param name="reader">The XML Reader containing the token request.</param>
        public RequestSecurityToken(XmlReader reader)
        {
            // Parse the message
            this.ParseRST(reader);

            // Do intradependent parameter checks
            if (this.RequestType == null)
            {
                // This is a required parameter, per the WS-Trust February 2005
                // specification. However, CardSpace.v1 does not always emit this value.
                //
                // throw new InvalidRequestFaultException();
            }
            
            if ((this.KeyType == Constants.WSIdentity.KeyTypes.NoProofKey &&
                 (this.KeySize == null &&
                  this.EncryptWith == null &&
                  this.SignWith == null))
                ||
                (this.KeyType == Constants.WSTrust.KeyTypes.Asymmetric &&
                 (this.KeySize != null &&
                  this.EncryptWith != null &&
                  this.SignWith != null)))
            {
                // Key types are ones we support
            }
            else
            {
                throw new InvalidProofKeyException();
            }
        }
        #endregion

        #region Delegates
        /// <summary>
        /// A delegate to handle reading sections of the RST.
        /// </summary>
        /// <param name="reader">An XmlReader containing the RST.</param>       
        private delegate void ElementHandler(XmlReader reader);
        #endregion

        #region Properties
        /// <summary>
        /// Gets the context for this request.
        /// </summary>
        public string Context
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets the key type required for this request.
        /// </summary>
        public string KeyType
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets the claims requested.
        /// </summary>
        public ReadOnlyCollection<string> RequestedClaims
        {        
            get;
            private set;
        }

        /// <summary>
        /// Gets the <see cref="SecurityKeyIdentifier"/> to use for this request.
        /// </summary>
        public SecurityKeyIdentifier UseKey
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets the identifier of the Information Card used in this request.
        /// </summary>
        public string CardId
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets the version of the Information Card used in this request.
        /// </summary>
        public int CardVersion
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets the relying party endpoint for this request.
        /// </summary>
        /// <remarks>
        /// Relying party details are only sent when the card on the client machine has a RequiresAppliesTo value 
        /// of true and the policy bindings for the STS have included <useManagedPresentation />, for example;
        /// <code>
        /// <binding name='UserNameBinding'>
        ///     ....
        ///     <useManagedPresentation />
        ///     ....
        /// </binding>
        /// </code>
        /// </remarks>
        public EndpointAddress RelyingPartyEndpoint
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets the PPID suggested by the identity selector, if any.
        /// </summary>
        public string PPID
        {
            get; 
            private set;
        }

        /// <summary>
        /// Gets a value indicating whether the request asked for a Display Token.
        /// </summary>
        public bool DisplayTokenRequested
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the type of security token requested.
        /// </summary>
        public string TokenType
        {
            get
            {
                return string.IsNullOrEmpty(this.tokenType) ? DefaultTokenType : this.tokenType;
            }

            private set
            {
                this.tokenType = value;
            }
        }

        /// <summary>
        /// Gets or sets the type of the request.
        /// </summary>
        private string RequestType
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the size of the key requested.
        /// </summary>
        private string KeySize
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the thumbprint of the certificate to encrypt with.
        /// </summary>
        private string EncryptWith
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the thumbprint of the certificate to sign the RSTR with.
        /// </summary>
        private string SignWith
        {
            get;
            set;
        }
        #endregion

        #region Common Parsing Functions
        /// <summary>
        /// Extracts a string value from the current XML element in the specified reader, ignoring all attributes,
        /// then moves the reader past the end of the current element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> to read the element value from.</param>
        /// <returns>The value of the current element in the XML reader.</returns>
        /// <exception cref="NotSupportedException">Thrown if the value of the current element is not one of the expected values, if any.</exception>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="reader"/> is null.</exception>
        private static string ConsumeSimpleString(XmlReader reader)
        {
            if (null == reader)
            {
                throw new ArgumentNullException("reader");
            }

            // No attributes are handled
            ReadAttribute(reader, null, null);

            // Extract the element content and verify it is expected value, if applicable
            string presentedValue = reader.ReadString();
            string expectedValue;
            if (expectedValues.TryGetValue(reader.LocalName, out expectedValue))
            {
                if (presentedValue == null ||
                    string.Compare(expectedValue, presentedValue, StringComparison.Ordinal) != 0)
                {
                    throw new NotSupportedException(Resources.ErrorUnexpectedValue);
                }
            }

            reader.ReadEndElement();
            return presentedValue;
        }

        /// <summary>
        /// Skips over the current element in the specified XML reader.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the current element to skip over.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="reader"/> is null.</exception>
        private static void IgnoreElement(XmlReader reader)
        {
            if (null == reader)
            {
                throw new ArgumentNullException("reader");
            }

            reader.Skip();
        }

        /// <summary>
        /// Reads the specified attribute from the current element, without moving the reader forward.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> to read the attribute from.</param>
        /// <param name="namespaceUri">The namespace URI for the required attribute.</param>
        /// <param name="attributeName">The name of the attribute required.</param>
        /// <returns>The value of the specified attribute.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="reader"/> is null.</exception>
        /// <exception cref="XmlException">Thrown if the <paramref name="reader"/> is not positioned on a start element or
        /// if an unrecognised attribute is encountered.</exception>
        private static string ReadAttribute(XmlReader reader, string namespaceUri, string attributeName)
        {
            string attributeString = null;

            if (null == reader)
            {
                throw new ArgumentNullException("reader");
            }

            if (!reader.IsStartElement())
            {
                throw new XmlException(Resources.ErrorReaderNotAtStartElement);
            }

            if (reader.HasAttributes)
            {
                while (reader.MoveToNextAttribute())
                {
                    if (attributeName != null &&
                        reader.LocalName == attributeName &&
                        reader.NamespaceURI == namespaceUri)
                    {
                        attributeString = reader.Value;
                    }
                    else
                    {
                        if (reader.Prefix != "xmlns" && reader.LocalName != "xmlns")
                        {
                            throw new XmlException(string.Format(
                                CultureInfo.CurrentCulture,
                                Resources.ErrorUnrecognisedAttribute,
                                reader.LocalName,
                                reader.NamespaceURI));
                        }
                    }
                }

                reader.MoveToElement(); // Leave the reader on the start element
            }

            return attributeString;
        }
        #endregion

        #region Message Section Handlers
        /// <summary>
        /// Parses the Canonicalization Algorithm attribute.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        /// <remarks>If the value is to be used this method should no longer be static.</remarks>
        private static void CanonicalizationAlgorithmHandler(XmlReader reader)
        {
            // the value is not referenced later, no need to store, but still
            // read to validate expected value
            ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the encrypts with element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void EncryptWithHandler(XmlReader reader)
        {
           this.EncryptWith = ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the request type element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void RequestTypeHandler(XmlReader reader)
        {
            this.RequestType = ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the token type element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void TokenTypeHandler(XmlReader reader)
        {
            this.TokenType = ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the key size element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void KeySizeHandler(XmlReader reader)
        {
            this.KeySize = ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the key type element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void KeyTypeHandler(XmlReader reader)
        {
            this.KeyType = ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the sign with element.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void SignWithHandler(XmlReader reader)
        {
            this.SignWith = ConsumeSimpleString(reader);
        }

        /// <summary>
        /// Parses the use key elements.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void UseKeyHandler(XmlReader reader)
        {
            // Ensure there are no attributes, since this code doesn't handle any
            ReadAttribute(reader, null, null);

            // Go to the next element
            reader.ReadStartElement();

            if (!reader.IsEmptyElement)
            {
                if (reader.IsStartElement())
                {
                    this.UseKey = WSSecurityTokenSerializer.DefaultInstance.ReadKeyIdentifier(reader);
                }

                reader.MoveToContent();
                reader.ReadEndElement();
            }
            else
            {
                reader.Read();
            }
        }      
      
        /// <summary>
        /// Extracts the details for the information card used to initiate the current request.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void InformationCardDetailsHandler(XmlReader reader)
        {
            XmlReader cardDetailsReader = reader.ReadSubtree();

            while (cardDetailsReader.Read())
            {
                if (cardDetailsReader.NodeType == XmlNodeType.Element &&
                    cardDetailsReader.NamespaceURI == Constants.WSIdentity.Namespace.Uri)
                {
                    if (cardDetailsReader.LocalName == Constants.WSIdentity.InformationCardReference.Elements.CardId)
                    {
                        this.CardId = ConsumeSimpleString(cardDetailsReader);
                    }

                    if (cardDetailsReader.LocalName == Constants.WSIdentity.InformationCardReference.Elements.CardVersion)
                    {
                        int cardVersion;
                        if (!int.TryParse(ConsumeSimpleString(cardDetailsReader), out cardVersion))
                        {
                            throw new NotSupportedException(Resources.ErrorUnexpectedCardVersion);
                        }

                        this.CardVersion = cardVersion;
                    }
                }
            }

            cardDetailsReader.Close();
            reader.ReadEndElement();
        }

        /// <summary>
        /// Iterates through the claims section and stores the requested claims.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void ClaimsHandler(XmlReader reader)
        {
            List<string> requestedClaims = new List<string>();

            XmlReader claimsReader = reader.ReadSubtree();
            while (claimsReader.Read())
            {
                if (claimsReader.NodeType == XmlNodeType.Element &&
                    claimsReader.NamespaceURI == Constants.WSIdentity.Namespace.Uri &&
                    claimsReader.LocalName == Constants.WSIdentity.Claims.Elements.ClaimType)
                {
                    string claimURI = null;
                    if (claimsReader.HasAttributes)
                    {
                        while (claimsReader.MoveToNextAttribute())
                        {
                            if (string.IsNullOrEmpty(claimsReader.LocalName))
                            {
                                throw new NotSupportedException(Resources.ErrorUnexpectedValue);
                            }

                            if (claimsReader.LocalName == Constants.WSIdentity.Claims.ClaimType.Attributes.Uri)
                            {
                                claimURI = claimsReader.Value;
                            }

                            /* else if (reader.LocalName == Constants.WSIdentity.Claims.ClaimType.Attributes.Optional)
                             * { 
                             *      System.Diagnostics.Debug.WriteLine("Optional =" + Convert.ToBoolean(reader.Value));
                             * }
                             */
                        }

                        if (!string.IsNullOrEmpty(claimURI))
                        {
                            requestedClaims.Add(claimURI);
                        }
                    }
                    else
                    {
                        throw new NotSupportedException(Resources.ErrorClaimHasNoAttributes);
                    }
                }                
            }

            claimsReader.Close();
            reader.ReadEndElement();
            this.RequestedClaims = requestedClaims.AsReadOnly();
        }

        /// <summary>
        /// Handles the ClientPseudonym Elements
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void ClientPseudonymHandler(XmlReader reader)
        {
            XmlReader clientPseudonymReader = reader.ReadSubtree();
            while (clientPseudonymReader.Read())
            {
                if (clientPseudonymReader.NodeType == XmlNodeType.Element &&
                    clientPseudonymReader.NamespaceURI == Constants.WSIdentity.ClientPseudonym.Namespace.Uri &&
                    clientPseudonymReader.LocalName == Constants.WSIdentity.ClientPseudonym.Elements.PPID)
                {
                    this.PPID = ConsumeSimpleString(clientPseudonymReader);
                }
            }

            clientPseudonymReader.Close();
            reader.ReadEndElement();
        }

        /// <summary>
        /// Handles any WSPolicy Elements
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void WSPolicyHandler(XmlReader reader)
        {
            XmlReader wsPolicyReader = reader.ReadSubtree();

            while (wsPolicyReader.Read())
            {
                if (wsPolicyReader.NodeType == XmlNodeType.Element &&
                    wsPolicyReader.NamespaceURI == Constants.WSAddressing.Namespace.Uri &&
                    wsPolicyReader.LocalName == Constants.WSAddressing.Elements.EndpointReference)
                {
                    this.RelyingPartyEndpoint =
                        EndpointAddress.ReadFrom(XmlDictionaryReader.CreateDictionaryReader(wsPolicyReader));
                }
            }

            wsPolicyReader.Close();
            reader.ReadEndElement();
        }

        /// <summary>
        /// Handles the presence of the DisplayToken element, which indicates a display token has been requested.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> containing the token.</param>
        private void DisplayTokenRequestHandler(XmlReader reader)
        {
            this.DisplayTokenRequested = true;
            reader.Skip();
        }
        #endregion

        #region Parsing Code
        /// <summary>
        /// Intialises the handlers for various sections of the RST.
        /// </summary>
        private void InitializeHandlers()
        {
            // Elements that are accepted and handled
            this.wsTrustElements.Add(Constants.WSTrust.Elements.RequestType, this.RequestTypeHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.TokenType, this.TokenTypeHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.CanonicalizationAlgorithm, CanonicalizationAlgorithmHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.KeyType, this.KeyTypeHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.KeySize, this.KeySizeHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.EncryptWith, this.EncryptWithHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.SignWith, this.SignWithHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.UseKey, this.UseKeyHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.Claims, this.ClaimsHandler);
            this.wsTrustElements.Add(Constants.WSTrust.Elements.EncryptionAlgorithm, IgnoreElement);

            this.wsPolicyElements.Add(Constants.WSPolicy.Elements.AppliesTo, this.WSPolicyHandler);

            this.wsIdentityElements.Add(Constants.WSIdentity.Elements.ClientPseudonym, this.ClientPseudonymHandler);
            this.wsIdentityElements.Add(Constants.WSIdentity.Elements.InformationCardReference, this.InformationCardDetailsHandler);

            this.wsIdentityElements.Add(Constants.WSIdentity.Elements.RequestDisplayToken, this.DisplayTokenRequestHandler);
        }

        /// <summary>
        /// Parses a Request for Security Token message from the specified XML Reader and sets the various properties extracted from the message.
        /// </summary>
        /// <param name="reader">The <see cref="XmlReader"/> to extract the RST from.</param>
        /// <exception cref="XmlException">Thrown if the message type is incorrect or if an unexpected element is encountered during parsing.</exception>
        private void ParseRST(XmlReader reader)
        {
            // Initialize routines to help parse the message
            this.InitializeHandlers();

            // Verify the message type and extract the context, if any
            if (reader.LocalName != Constants.WSTrust.Elements.RequestSecurityToken || reader.NamespaceURI != Constants.WSTrust.Namespace.Uri)
            {
                throw new XmlException(Resources.ErrorIncorrectElement);
            }

            this.Context = ReadAttribute(reader, "", Constants.WSTrust.Attributes.Context);
            reader.Read();

            // Move from node to node until done
            while (!reader.EOF)
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        // Handle the fixed values and verify values are as expected for
                        // the sample scenarios.
                        ElementHandler handler;
                        if ((reader.NamespaceURI == Constants.WSTrust.Namespace.Uri &&
                             this.wsTrustElements.TryGetValue(reader.LocalName, out handler)) ||
                            (reader.NamespaceURI == Constants.WSIdentity.Namespace.Uri &&
                             this.wsIdentityElements.TryGetValue(reader.LocalName, out handler)) ||
                            (reader.NamespaceURI == Constants.WSPolicy.Namespace.Uri &&
                             this.wsPolicyElements.TryGetValue(reader.LocalName, out handler)))
                        {
                            // this is an element that we expect and handle
                            handler(reader);
                        }
                        else
                        {
                            throw new XmlException(Resources.ErrorUnexpectedElement);
                        }

                        break;

                    case XmlNodeType.Whitespace:
                    case XmlNodeType.SignificantWhitespace:
                    case XmlNodeType.EndElement:
                    case XmlNodeType.Comment:
                        reader.Read();
                        break;

                    case XmlNodeType.Text:
                    case XmlNodeType.CDATA:
                    case XmlNodeType.ProcessingInstruction:
                    case XmlNodeType.Document:
                        throw new XmlException(Resources.ErrorUnexpectedElement);
                }
            }
        }
        #endregion
    }
}